Skip to content

Commit fe11b11

Browse files
authored
Merge pull request #6551 from escattone/prevent-invalid-emails-from-spoiling-rest
only send messages to valid emails
2 parents 216d732 + 34b7b2a commit fe11b11

File tree

2 files changed

+64
-3
lines changed

2 files changed

+64
-3
lines changed

kitsune/sumo/email_utils.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from django.conf import settings
55
from django.contrib.sites.models import Site
66
from django.core import mail
7+
from django.core.exceptions import ValidationError
78
from django.core.mail import EmailMultiAlternatives
9+
from django.core.validators import validate_email
810
from django.template.loader import render_to_string
911
from django.test.client import RequestFactory
1012
from django.utils import translation
@@ -15,13 +17,48 @@
1517
log = logging.getLogger("k.email")
1618

1719

20+
def normalize_gmail(email: str) -> str:
21+
"""
22+
Return the given email with periods removed from its "local part"
23+
if it's a gmail address, otherwise return it unchanged.
24+
"""
25+
if email.lower().endswith("@gmail.com"):
26+
# Periods are ignored in the "local part" of gmail addresses.
27+
# We need to remove them before using Django's "validate_email",
28+
# otherwise it might incorrectly raise a ValidationError.
29+
local_part, domain = email.rsplit("@", maxsplit=1)
30+
return f"{local_part.replace('.', '')}@{domain}"
31+
return email
32+
33+
34+
def is_valid_email(email: str) -> bool:
35+
"""
36+
Returns True if the given email address is valid, False otherwise.
37+
"""
38+
try:
39+
validate_email(normalize_gmail(email))
40+
except ValidationError:
41+
return False
42+
return True
43+
44+
1845
def send_messages(messages):
19-
"""Sends a bunch of EmailMessages."""
46+
"""Sends a bunch of email messages."""
2047
if not messages:
2148
return
2249

50+
# Only send each message to its valid recipients,
51+
# excluding messages without any valid recipients.
52+
cleaned_messages = []
53+
for message in messages:
54+
# Remove invalid emails and normalize gmails.
55+
cleaned_to = [normalize_gmail(email) for email in message.to if is_valid_email(email)]
56+
if cleaned_to:
57+
message.to = cleaned_to
58+
cleaned_messages.append(message)
59+
2360
with mail.get_connection(fail_silently=True) as conn:
24-
conn.send_messages(list(messages))
61+
conn.send_messages(cleaned_messages)
2562

2663

2764
def safe_translation(f):

kitsune/sumo/tests/test_email_utils.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
from django.conf import settings
44
from django.contrib.sites.models import Site
5+
from django.core.mail import EmailMultiAlternatives
56
from django.utils.functional import lazy
67
from django.utils import translation
78
from django.utils.translation import get_language
89

9-
from kitsune.sumo.email_utils import emails_with_users_and_watches, safe_translation
10+
from kitsune.sumo.email_utils import emails_with_users_and_watches, safe_translation, send_messages
1011
from kitsune.sumo.tests import TestCase
1112
from kitsune.users.tests import UserFactory
1213

@@ -132,3 +133,26 @@ def test_styles_inlining(self):
132133
for m in msg:
133134
tag = '<a href="https://%s/test" style="color:#000">Hyperlink</a>'
134135
self.assertIn(tag % Site.objects.get_current().domain, str(m.message()))
136+
137+
138+
class SendMessagesTests(TestCase):
139+
140+
@patch("kitsune.sumo.email_utils.mail")
141+
def test_send_messages(self, mock_mail):
142+
from_email = "notifications@support.mozilla.org"
143+
messages = [
144+
EmailMultiAlternatives("Test", "Testing", from_email, ["beatles"]),
145+
EmailMultiAlternatives(
146+
"Test", "Testing", from_email, ["george.harrison.@gmail.com", "ringo"]
147+
),
148+
EmailMultiAlternatives("Test", "Testing", from_email, ["paul.mccartney.@gmail.com"]),
149+
EmailMultiAlternatives(
150+
"Test", "Testing", from_email, ["ringo@beatles.com", "george@beatles.com"]
151+
),
152+
]
153+
send_messages(messages)
154+
send_messages_mock = mock_mail.get_connection().__enter__().send_messages
155+
send_messages_mock.assert_called_once_with([messages[1], messages[2], messages[3]])
156+
self.assertEqual(messages[1].to, ["georgeharrison@gmail.com"])
157+
self.assertEqual(messages[2].to, ["paulmccartney@gmail.com"])
158+
self.assertEqual(messages[3].to, ["ringo@beatles.com", "george@beatles.com"])

0 commit comments

Comments
 (0)